Skip to content

Fix concurrent-extension race in MMapDirectory.Map, #1090 - #1263

Merged
paulirwin merged 2 commits into
apache:masterfrom
paulirwin:issue/1090
Apr 19, 2026
Merged

Fix concurrent-extension race in MMapDirectory.Map, #1090#1263
paulirwin merged 2 commits into
apache:masterfrom
paulirwin:issue/1090

Conversation

@paulirwin

Copy link
Copy Markdown
Contributor
  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a change, please open an issue to discuss the change or find an existing issue.

Fixes a concurrent race bug in MMapDirectory.Map.

Fixes #1090

Description

When a file is being appended to concurrently (e.g. by an IndexWriter that still holds a write handle, as in TestNeverDelete), MemoryMappedFile.CreateFromFile can throw ArgumentOutOfRangeException("capacity") with the message The capacity may not be smaller than the file size. This happens because the BCL performs an internal stat and compares the current file size against the capacity we passed — which was computed from fc.Length moments earlier. If the file grows in between, the check fails.

The fix, in MMapDirectory.Map:

  • Take the max of the caller-supplied length and a fresh fc.Length read as the initial capacity.
  • Retry on ArgumentOutOfRangeException (up to 5 attempts) with the latest observed length, since the window between our stat and the BCL's internal stat still allows further growth.

The buffer-sizing loop continues to use the caller-supplied length, so an oversized capacity is harmless — we never expose the extra bytes.

Testing

Added TestMultiMMap.TestOpenInputConcurrentFileExtension_Issue1090, a stress-based regression test that spins a background thread extending a small file (capped at 1 MiB) while the foreground repeatedly calls MMapDirectory.OpenInput. On unpatched code, this reproduces the
exact stack trace from #1090 within milliseconds. With the fix, the test passes.

To avoid the "silently passes because the race didn't fire" trap, two internal counters (s_capacityRetryCount, s_maxCapacityAttemptsObserved) record retry activity, and the test:

  • Asserts the retry counter incremented during the run — proving the fix was actually exercised.
  • Marks itself inconclusive (not passed) if the race doesn't fire within 15 seconds, so a timing-unlucky run is visible rather than a false green.

Observed on macOS/ARM across 5 runs: race triggered within 4–120 iterations; 1 retry always sufficed (maxAttemptsObserved = 2). The configured max of 5 is defensive padding.

Test is marked [Slow].

AI: Generated with Claude Code Opus 4.7.

When a file is being appended to concurrently (e.g. by an IndexWriter
that still holds a write handle), MemoryMappedFile.CreateFromFile can
throw ArgumentOutOfRangeException("capacity") because its internal stat
observes a file size greater than the capacity we computed from
fc.Length moments earlier. Take the max of the caller-supplied length
and a fresh fc.Length read as the capacity, and retry with an updated
length on that specific failure. Adds test-only counters and a
stress-based regression test in TestMultiMMap.
@paulirwin paulirwin added the notes:bug-fix Contains a fix for a bug label Apr 17, 2026
@paulirwin
paulirwin requested a review from Copilot April 17, 2026 18:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a concurrency race in Lucene.Net.Store.MMapDirectory.Map() where a file growing between reading fc.Length and MemoryMappedFile.CreateFromFile()’s internal size check could throw ArgumentOutOfRangeException("capacity") (issue #1090).

Changes:

  • Adds a capacity selection + retry loop around MemoryMappedFile.CreateFromFile() to handle concurrent file extension.
  • Introduces internal counters to observe retry behavior (used by tests).
  • Adds a slow stress/regression test that extends a file concurrently while repeatedly calling MMapDirectory.OpenInput.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/Lucene.Net/Store/MMapDirectory.cs Adds retry logic for CreateFromFile() capacity race and internal counters for observability.
src/Lucene.Net.Tests/Store/TestMultiMMap.cs Adds a slow regression test that attempts to reproduce the concurrent-extension race from #1090.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Lucene.Net/Store/MMapDirectory.cs Outdated
Comment thread src/Lucene.Net.Tests/Store/TestMultiMMap.cs
Narrow the ArgumentOutOfRangeException retry filter to
ParamName == "capacity" so unrelated argument errors aren't masked,
and mark the regression test NonParallelizable since it relies on
static counters on MMapDirectory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@paulirwin
paulirwin merged commit 1a1103d into apache:master Apr 19, 2026
211 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

notes:bug-fix Contains a fix for a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Random test failure: TestNeverDelete.TestIndexing

2 participants